NSURLSession最全攻略
本文字数:11276字
预计阅读时间:32分钟
NSURLSession
NSURLSession
在iOS7
中推出,NSURLSession
的推出旨在替换之前的NSURLConnection
,NSURLSession
的使用相对于之前的NSURLConnection
更简单,而且不用处理Runloop
相关的东西。RFC 7540
标准发布了http 2.0
版本,http 2.0
版本中包含很多新的特性,在传输速度上也有很明显的提升。NSURLSession
从iOS9.0
开始,对http 2.0
提供了支持。NSURLSession
部分构成:NSURLSession:请求会话对象,可以用系统提供的单例对象,也可以自己创建。
NSURLSessionConfiguration:对
session
会话进行配置,一般都采用default
。NSURLSessionTask:负责执行具体请求的
task
,由session
创建。
NSURLSession
有三种方式创建:session
的task
共享连接和请求信息。NSURLSessio
n初始化时传入一个NSURLSessionConfiguration
,这样可以自定义请求头、cookie
等信息。delegate
来设置回调对象和回调的线程。NSURLSession
发起一个网络请求也比较简单。创建一个
NSURLSessionConfiguration
配置请求。通过
Configuration
创建NSURLSession
对象。通过
session
对象发起网络请求,并获取task
对象。调用
[task resume]
方法发起网络请求。
NSURLSession *session = [NSURLSession sessionWithConfiguration:config
delegate:self
delegateQueue:[NSOperationQueue mainQueue]];
NSURLSessionDataTask *task = [session dataTaskWithURL:[NSURL URLWithString:@"http://www.baidu.com"]];
[task resume];
NSURLSession
发起的每个请求,都会被封装为一个NSURLSessionTask
任务,但一般不会直接是NSURLSessionTask
类,而是基于不同任务类型,被封装为其对应的子类。NSURLSessionDataTask
:处理普通的Get
、Post
请求。NSURLSessionUploadTask
:处理上传请求,可以传入对应的上传文件或路径。NSURLSessionDownloadTask
:处理下载地址,提供断点续传功能的cancel
方法。
NSURLSessionTask
中,下面是一些关键方法或属性。currentRequest
originalRequest
是一样的,除非发生重定向才会有所区别。originalRequest
taskIdentifier
session
下,task
的唯一标示,多个session
之间可能存在相同的标识。priority
task
中可以设置优先级,但这个属性并不代表请求的优先级,而是一个标示。官方已经说明,NSURLSession
并没有提供API
可以改变请求的优先级。state
KVO
的方式监听状态的改变。- resume
task
默认是挂起的,需要手动调用resume
才可以开始请求。- suspend
NSURLRequest
设置的timeout
时间,调用resume
就是继续请求。- cancel
URLSession:task:didCompleteWithError:
方法。NSURLSession
提供有普通创建task的方式,创建后可以通过重写代理方法,获取对应的回调和参数。这种方式对于请求过程比较好控制。- (NSURLSessionUploadTask *)uploadTaskWithRequest:(NSURLRequest *)request fromFile:(NSURL *)fileURL;
- (NSURLSessionDownloadTask *)downloadTaskWithRequest:(NSURLRequest *)request;
NSURLSession
也提供了block
的方式创建task
,创建方式简单如AFN
,直接传入URL
或NSURLRequest
,即可直接在block
中接收返回数据。和普通创建方式一样,block
的创建方式创建后默认也是suspend
的状态,需要调用resume
开始任务。completionHandler
和delegate
是互斥的,completionHandler
的优先级大于delegate
。相对于普通创建方法,block
方式更偏向于面向结果的创建,可以直接在completionHandler
中获取返回结果,但不能控制请求过程。- (NSURLSessionUploadTask *)uploadTaskWithRequest:(NSURLRequest *)request fromData:(nullable NSData *)bodyData completionHandler:(void (^)(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error))completionHandler;
- (NSURLSessionDownloadTask *)downloadTaskWithURL:(NSURL *)url completionHandler:(void (^)(NSURL * _Nullable location, NSURLResponse * _Nullable response, NSError * _Nullable error))completionHandler;
session
对应的所有task
,方法区别在于回调的参数不同。以getTasksWithCompletionHandler
为例,在AFN
中的应用是用来获取当前session
的task
,并将AFURLSessionManagerTaskDelegate
的回调都置为nil
,以防止崩溃。- (void)getAllTasksWithCompletionHandler:(void (^)(NSArray<__kindof NSURLSessionTask *> *tasks))completionHandler);
NSURLSession
时可以指定线程,如果不指定线程,则completionHandler
和delegate
的回调方法,都会在子线程中执行。NSURLSession
时指定了delegateQueue
,则回调会在指定的队列中执行,如果指定的是mainQueue
,则回调在主线程中执行,这样就避免了切换线程的问题。NSURLSession
的代理方法这里就不详细列举了,方法命名遵循苹果一贯见名知意的原则,用起来很简单。这里介绍一下NSURLSession
的代理继承结构。NSURLSession
中定义了一系列代理,并遵循上面的继承关系。根据继承关系和代理方法的声明,如果执行某项任务,只需要遵守其中的某个代理即可。Post
请求,则遵守NSURLSessionDataDelegate
,执行下载任务则遵循NSURLSessionDownloadDelegate
,父级代理定义的都是公共方法。HTTP
协议中定义了例如301等重定向状态码,通过下面的代理方法,可以处理重定向任务。发生重定向时可以根据response
创建一个新的request
,也可以直接用系统生成的request
,并在completionHandler
回调中传入,如果想终止这次重定向,在completionHandler
传入nil
即可。task:(NSURLSessionTask *)task
willPerformHTTPRedirection:(NSHTTPURLResponse *)response
newRequest:(NSURLRequest *)request
completionHandler:(void (^)(NSURLRequest *))completionHandler
{
NSURLRequest *redirectRequest = request;
if (self.taskWillPerformHTTPRedirection) {
redirectRequest = self.taskWillPerformHTTPRedirection(session, task, response, request);
}
if (completionHandler) {
completionHandler(redirectRequest);
}
}
NSURLSessionConfiguration
NSURLSessionConfiguration
负责对NSURLSession
初始化时进行配置,通过NSURLSessionConfiguration
可以设置请求的Cookie
、密钥、缓存、请求头等参数,将网络请求的一些配置参数从NSURLSession
中分离出来。NSURLSession *session = [NSURLSession sessionWithConfiguration:config
delegate:self
delegateQueue:[NSOperationQueue mainQueue]];
NSURLSessionConfiguration
提供三种初始化方法,下面是请求的方法的一些解释。NSURLSessionConfiguration
提供defaultSessionConfiguration
的方式创建,但这并不是单例方法,而是类方法,创建的是不同对象。通过这种方式创建的configuration
,并不会共享cookie
、cache
、密钥等,而是不同configuration
都需要单独设置。configuration
,通过这种方式创建的对象,和普通的对象主要区别在于URLCache
、URLCredentialStorage
、HTTPCookieStorage
上面。同样的,Ephemeral
也不是单例方法,而只是类方法。Ephemeral <__NSCFMemoryURLCredentialStorage: 0x600001bc8320>
HTTPCookieStorage
Ephemeral <NSHTTPCookieStorage cookies count:0>
Ephemeral
方式创建的config
进行打印的话,可以看到变量类型明显区别于其他类型,并且在打印信息前面会有Ephemeral
的标示。通过Ephemeral
的方式创建的config
,不会产生持久化信息,可以很好保护请求的数据安全性。identifier
方式一般用于恢复之前的任务,主要用于下载。如果一个下载任务正在进行中,程序被kill调,可以在程序退出之前保存identifier
。下次进入程序后通过identifier
恢复之前的任务,系统会将NSURLSession
及NSURLSessionConfiguration
和之前的下载任务进行关联,并继续之前的任务。session
请求间的超时时间,这个超时时间并不是请求从开始到结束的时间,而是两个数据包之间的时间间隔。当任意请求返回后这个值将会被重置,如果在超时时间内未返回则超时。单位为秒,默认为60秒。NSURLSessionConfiguration
对象,会共享请求头、缓存、cookie
、Credential
,通过Configuration
创建的NSURLSession
,也会拥有对应的请求信息。Confuguration
配置的NSURLSession
,请求头都会带有设置的信息。HTTP
请求的Cookie
管理器。如果是通过sharedSession
或backgroundConfiguration
创建的NSURLSession
,默认使用sharedHTTPCookieStorage
的Cookie
数据。如果不想使用Cookie
,则直接设置为nil
即可,也可以手动设置为自己的CookieStorage
。sharedSession
或backgroundConfiguration
创建的NSURLSession
,默认使用sharedCredentialStorage
的证书。如果不想使用证书,可以直接设置为nil
,也可以自己创建证书管理器。nil
,对于NSURLCache
这个类我没有研究过,不太了解。NSURLRequest
中可以设置cachePolicy
请求缓存策略,这里不对具体值做详细描述,默认值为NSURLRequestUseProtocolCachePolicy
使用缓存。NSURLSessionConfiguration
可以设置处理缓存的对象,我们可以手动设置自定义的缓存对象,如果不设置的话,默认使用系统的sharedURLCache
单例缓存对象。经过configuration
创建的NSURLSession
发出的请求,NSURLRequest
都会使用这个NSURLCache
来处理缓存。NSURLCache
提供了Memory
和Disk
的缓存,在创建时需要为其分别指定Memory
和Disk的大小,以及存储的文件位置。使用NSURLCache
不用考虑磁盘空间不够,或手动管理内存空间的问题,如果发生内存警告系统会自动清理内存空间。但是NSURLCache
提供的功能非常有限,项目中一般很少直接使用它来处理缓存数据,还是用数据库比较多。diskCapacity:30 * 1024 * 1024
directoryURL:[NSURL URLWithString:filePath]];
NSURLCache
还有一个好处,就是可以由服务端来设置资源过期时间,在请求服务端后,服务端会返回Cache-Control
来说明文件的过期时间。NSURLCache
会根据NSURLResponse
来自动完成过期时间的设置。NSURLSession
的最大连接数,通过此方法创建的NSURLSession
和服务端的最大连接数量不会超出这里设置的数量。苹果为我们设置的iOS
端默认为4,Mac
端默认为6。连接复用
HTTP
是基于传输层协议TCP
的,通过TCP
发送网络请求都需要先进行三次握手,建立网络请求后再发送数据,请求结束时再经历四次挥手。HTTP1.0
开始支持keep-alive
,keep-alive
可以保持已经建立的链接,如果是相同的域名,在请求连接建立后,后面的请求不会立刻断开,而是复用现有的连接。从HTTP1.1
开始默认开启keep-alive
。keep-alive
的话,响应客户端请求时,也会在响应头中加上相同的字段。keep-alive
,可以在请求头中加上下面的字段,但一般不推荐这么做。NSURLSession
来进行网络请求的话,需要使用同一个NSURLSession
对象,如果创建新的session对象则不能复用之前的链接。keep-alive
可以保持请求的连接,苹果允许在iOS
上最大保持有4个连接,Mac
则是6个连接。在HTTP1.1
中,基于keep-alive
,还可以将请求进行管线化。和相同后端服务,TCP
层建立的链接,一般都需要前一个请求返回后,后面的请求再发出。但pipeline
就可以不依赖之前请求的响应,而发出后面的请求。pipeline
依赖客户端和服务器都有实现,服务端收到客户端的请求后,要按照先进先出的顺序进行任务处理和响应。pipeline
依然存在之前非pipeline
的问题,就是前面的请求如果出现问题,会阻塞当前连接影响后面的请求。pipeline
对于请求大文件并没有提升作用,只是对于普通请求速度有提升。在NSURLSessionConfiguration
中可以设置HTTPShouldUsePipelining
为YES
,开启管线化,此属性默认为NO
。NSURLSessionTaskMetrics
NSURLSessionTaskMetrics
类来进行检查,NSURLSessionTaskMetrics
是对应NSURLSessionTaskDelegate
的,每个task
结束时都会回调下面的方法,并且可以获得一个metrics
对象。task:(NSURLSessionTask *)task
didFinishCollectingMetrics:(NSURLSessionTaskMetrics *)metrics;
NSURLSessionTaskMetrics
可以很好的帮助我们分析网络请求的过程,以找到耗时原因。除了这个类之外,NSURLSessionTaskTransactionMetrics
类中承载了更详细的数据。transactionMetrics
数组中每一个元素都对应着当前task
的一个请求,一般数组中只会有一个元素,如果发生重定向等情况,可能会存在多个元素。taskInterval
记录了当前task
从开始请求到最后完成的总耗时,NSDateInterval
中包含了startDate
、endDate
和duration
耗时时间。redirectCount
记录了重定向次数,在进行下载请求时一般都会进行重定向,来保证下载任务能由后端最合适的节点来处理。NSURLSessionTaskTransactionMetrics
中的属性都是用来做统计的,功能都是记录某个值,并没有逻辑上的意义。所以这里就对一些主要的属性做一下解释,基本涵盖了大部分属性,其他就不管了。NSURLSessionTaskTransactionMetrics
的属性在请求过程中处于什么位置。@property (copy, readonly) NSURLRequest *request;
// 响应对象,请求失败可能会为nil
@property (nullable, copy, readonly) NSURLResponse *response;
// 请求开始时间
@property (nullable, copy, readonly) NSDate *fetchStartDate;
// DNS解析开始时间
@property (nullable, copy, readonly) NSDate *domainLookupStartDate;
// DNS解析结束时间,如果解析失败可能为nil
@property (nullable, copy, readonly) NSDate *domainLookupEndDate;
// 开始建立TCP连接时间
@property (nullable, copy, readonly) NSDate *connectStartDate;
// 结束建立TCP连接时间
@property (nullable, copy, readonly) NSDate *connectEndDate;
// 开始TLS握手时间
@property (nullable, copy, readonly) NSDate *secureConnectionStartDate;
// 结束TLS握手时间
@property (nullable, copy, readonly) NSDate *secureConnectionEndDate;
// 开始传输请求数据时间
@property (nullable, copy, readonly) NSDate *requestStartDate;
// 结束传输请求数据时间
@property (nullable, copy, readonly) NSDate *requestEndDate;
// 接收到服务端响应数据时间
@property (nullable, copy, readonly) NSDate *responseStartDate;
// 服务端响应数据传输完成时间
@property (nullable, copy, readonly) NSDate *responseEndDate;
// 网络协议,例如http/1.1
@property (nullable, copy, readonly) NSString *networkProtocolName;
// 请求是否使用代理
@property (assign, readonly, getter=isProxyConnection) BOOL proxyConnection;
// 是否复用已有连接
@property (assign, readonly, getter=isReusedConnection) BOOL reusedConnection;
// 资源标识符,表示请求是从Cache、Push、Network哪种类型加载的
@property (assign, readonly) NSURLSessionTaskMetricsResourceFetchType resourceFetchType;
// 本地IP
@property (nullable, copy, readonly) NSString *localAddress;
// 本地端口号
@property (nullable, copy, readonly) NSNumber *localPort;
// 远端IP
@property (nullable, copy, readonly) NSString *remoteAddress;
// 远端端口号
@property (nullable, copy, readonly) NSNumber *remotePort;
// TLS协议版本,如果是http则是0x0000
@property (nullable, copy, readonly) NSNumber *negotiatedTLSProtocolVersion;
// 是否使用蜂窝数据
@property (readonly, getter=isCellular) BOOL cellular;
http
的下载请求,统计得到的数据。设备是Xcode
模拟器,网络环境是WiFi
。(Response) <NSHTTPURLResponse: 0x600000ed9420> { URL: http://vfx.mtime.cn/Video/2017/03/31/mp4/170331093811717750.mp4 } { Status Code: 200, Headers {
"Accept-Ranges" = (
bytes
);
Age = (
1063663
);
"Ali-Swift-Global-Savetime" = (
1575358696
);
Connection = (
"keep-alive"
);
"Content-Length" = (
20472584
);
"Content-Md5" = (
"YM+JxIH9oLH6l1+jHN9pmQ=="
);
"Content-Type" = (
"video/mp4"
);
Date = (
"Tue, 03 Dec 2019 07:38:16 GMT"
);
EagleId = (
dbee142415764223598843838e
);
Etag = (
"\"60CF89C481FDA0B1FA975FA31CDF6999\""
);
"Last-Modified" = (
"Fri, 31 Mar 2017 01:41:36 GMT"
);
Server = (
Tengine
);
"Timing-Allow-Origin" = (
"*"
);
Via = (
"cache39.l2et2[0,200-0,H], cache6.l2et2[3,0], cache16.cn548[0,200-0,H], cache16.cn548[1,0]"
);
"X-Cache" = (
"HIT TCP_MEM_HIT dirn:-2:-2"
);
"X-M-Log" = (
"QNM:xs451;QNM3:71"
);
"X-M-Reqid" = (
"m0AAAP__UChjzNwV"
);
"X-Oss-Hash-Crc64ecma" = (
12355898484621380721
);
"X-Oss-Object-Type" = (
Normal
);
"X-Oss-Request-Id" = (
5DE20106F3150D38305CE159
);
"X-Oss-Server-Time" = (
130
);
"X-Oss-Storage-Class" = (
Standard
);
"X-Qnm-Cache" = (
Hit
);
"X-Swift-CacheTime" = (
2592000
);
"X-Swift-SaveTime" = (
"Sun, 15 Dec 2019 15:05:37 GMT"
);
} }
(Fetch Start) 2019-12-15 15:05:59 +0000
(Domain Lookup Start) 2019-12-15 15:05:59 +0000
(Domain Lookup End) 2019-12-15 15:05:59 +0000
(Connect Start) 2019-12-15 15:05:59 +0000
(Secure Connection Start) (null)
(Secure Connection End) (null)
(Connect End) 2019-12-15 15:05:59 +0000
(Request Start) 2019-12-15 15:05:59 +0000
(Request End) 2019-12-15 15:05:59 +0000
(Response Start) 2019-12-15 15:05:59 +0000
(Response End) 2019-12-15 15:06:04 +0000
(Protocol Name) http/1.1
(Proxy Connection) NO
(Reused Connection) NO
(Fetch Type) Network Load
(Request Header Bytes) 235
(Request Body Transfer Bytes) 0
(Request Body Bytes) 0
(Response Header Bytes) 866
(Response Body Transfer Bytes) 20472584
(Response Body Bytes) 20472584
(Local Address) 192.168.1.105
(Local Port) 63379
(Remote Address) 219.238.20.101
(Remote Port) 80
(TLS Protocol Version) 0x0000
(TLS Cipher Suite) 0x0000
(Cellular) NO
(Expensive) NO
(Constrained) NO
(Multipath) NO
FAQ
NSURLSession
对象并设置代理后,代理对象将会被强引用。根据苹果官方的注释来看,这个强持有并不会一直存在,而是在调用URLSession:didBecomeInvalidWithError:
方法后,会将delegate
释放。NSURLSession的invalidateAndCancel
或finishTasksAndInvalidate
方法,即可将强引用断开并执行didBecomeInvalidWithError:
代理方法,执行完成后session
就会无效不可以使用。也就是只有在session
无效时,才可以解除强引用的关系。session
会话invalid
,所以最好不要直接使用NSURLSession
,而是要对其进行一次二次封装,使用AFN3.0
的原因之一也在于此。文件上传
NSFileHandle
来进行文件读取。NSFileHandle
提供了一个偏移量的功能,我们可以将handle
的当前读取位置seek
到上次读取的位置,并设置本次读取长度,读取的文件就是我们指定文件的字节。if (self.maxSegment <= self.currentIndex) {
return nil;
}
if(!self.fileHandler){
NSString *filePath = [self uploadFile];
NSFileHandle *fileHandle = [NSFileHandle fileHandleForReadingAtPath:filePath];
self.fileHandler = fileHandle;
}
[self.fileHandler seekToFileOffset:(self.currentIndex) * self.segmentSize];
NSData *data = [self.fileHandler readDataOfLength:self.segmentSize];
return data;
}
multipart/from-data
,AFNetworking
对表单上传也有很有的支持。表单上传需要遵循下面的格式进行上传,boundary
是一个16进制字符串,可以是任何且唯一的。boundary
的功能用来进行字段分割,区分开不同的参数部分。multipart/from-data
规范定义在rfc2388,详细字段可以看一下规范。
--boundary
Content-Disposition: form-data; name="参数名"
参数值
--boundary
Content-Disposition:form-data;name=”表单控件名”;filename=”上传文件名”
Content-Type:mime type
要上传文件二进制数据
--boundary--
UTF-8
格式进行编码,服务端也采用相同的解码方式,则可以获得上传文件和信息。需要注意的是,换行符数量是固定的,这都是固定的协议格式,不要多或者少,会导致服务端解析失败。parameters:(NSDictionary *)parameters {
if (data.length == 0) {
return nil;
}
NSMutableData *formData = [NSMutableData data];
NSData *lineData = [@"\r\n" dataUsingEncoding:NSUTF8StringEncoding];
NSData *boundary = [kBoundary dataUsingEncoding:NSUTF8StringEncoding];
// 拼接上传参数
[parameters enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
[formData appendData:boundary];
[formData appendData:lineData];
NSString *thisFieldString = [NSString stringWithFormat:@"Content-Disposition: form-data; name=\"%@\"\r\n\r\n%@", key, obj];
[formData appendData:[thisFieldString dataUsingEncoding:NSUTF8StringEncoding]];
[formData appendData:lineData];
}];
// 拼接上传信息
[formData appendData:boundary];
[formData appendData:lineData];
NSString *thisFieldString = [NSString stringWithFormat:@"Content-Disposition: form-data; name=\"%@\"; filename=\"%@\"\r\nContent-Type: %@", @"name", @"filename", @"mimetype"];
[formData appendData:[thisFieldString dataUsingEncoding:NSUTF8StringEncoding]];
[formData appendData:lineData];
[formData appendData:lineData];
// 拼接上传文件
[formData appendData:data];
[formData appendData:lineData];
[formData appendData: [[NSString stringWithFormat:@"--%@--\r\n", kBoundary] dataUsingEncoding:NSUTF8StringEncoding]];
return formData;
}
Content-Type
和Content-Length
,否则会导致请求失败。其中Content-Length
并不是强制要求的,要看后端的具体支持情况。boundary
,这个boundary
和拼接上传文件的boundary
需要是同一个。服务端从请求头拿到boundary
,来解析上传文件。[request setValue:headerField forHTTPHeaderField:@"Content-Type"];
NSUInteger size = [[[NSFileManager defaultManager] attributesOfItemAtPath:uploadPath error:nil] fileSize];
headerField = [NSString stringWithFormat:@"%lu", size];
[request setValue:headerField forHTTPHeaderField:@"Content-Length"];
NSURLSessionUploadTask
,并调用resume
发起请求,实现对应的代理回调即可。NSURLSessionUploadTask *uploadTask = [self.backgroundSession uploadTaskWithRequest:request fromData:fromData];
[uploadTask resume];
// 请求完成后调用,无论成功还是失败
- (void)URLSession:(NSURLSession *)session
task:(NSURLSessionTask *)task
didCompleteWithError:(NSError *)error {
}
// 更新上传进度,会回调多次
- (void)URLSession:(NSURLSession *)session
task:(NSURLSessionTask *)task
didSendBodyData:(int64_t)bytesSent
totalBytesSent:(int64_t)totalBytesSent
totalBytesExpectedToSend:(int64_t)totalBytesExpectedToSend {
}
// 数据接收完成回调
- (void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)dataTask
didReceiveData:(NSData *)data {
}
// 处理后台上传任务,当前session的上传任务结束后会回调此方法。
- (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session {
}
fromData
的方式进行上传,并不支持后台上传。如果想实现后台上传,需要通过fromFile
的方式上传文件。不止如此,fromData
还有其他坑。- (NSURLSessionUploadTask *)uploadTaskWithRequest:(NSURLRequest *)request fromFile:(NSURL *)fileURL;
fromData:
的方式上传文件,内存涨上去之后一直不能降下来,无论是直接使用NSURLSession
还是AFNetworking
,都是这样的。小文件还好,不是很明显,如果是几百MB的大文件很明显就会有一个内存峰值,而且涨上去就不会降下来。WTF?fromData:
的上传改为fromFile:
,就可以解决内存不下降的问题。所以,我们可以把fromData:
的上传方式,理解为UIImage
的imageNamed
的方法,上传后NSData
文件会保存在内存中,不会被回收。而fromFile:
的方式是从本地加载文件,并且上传完成后可以被回收。而且如果想支持后台上传,就必须用fromFile:
的方式进行上传。fromFile:
的方式上传。NSString *filePath = [NSString stringWithFormat:@"%@/%ld", [self segmentDocumentPath], currentIndex];
BOOL write = [formData writeToFile:filePath atomically:YES];
// 创建分片文件夹
- (NSString *)segmentDocumentPath {
NSString *documentName = [fileName md5String];
NSString *filePath = [[SVPUploadCompressor compressorPath] stringByAppendingPathComponent:documentName];
BOOL needCreateDirectory = YES;
BOOL isDirectory = NO;
if ([[NSFileManager defaultManager] fileExistsAtPath:filePath isDirectory:&isDirectory]) {
if (isDirectory) {
needCreateDirectory = NO;
} else {
[[NSFileManager defaultManager] removeItemAtPath:filePath error:nil];
}
}
if (needCreateDirectory) {
[[NSFileManager defaultManager] createDirectoryAtPath:filePath
withIntermediateDirectories:YES
attributes:nil
error:nil];
}
return filePath;
}
fromFile:
方法传一个本地分片的路径进去,所以需要预先对文件进行分片,并保存在本地。在分片的同时,还需要拼接boundary
信息。MD5
来创建分片文件夹,分片文件命名通过下标来命名,并写入到本地。文件上传完成后,直接删除整个文件夹即可。当然,这些文件操作都是在异步线程中完成的,防止影响UI线程。fromFile:
后,上传文件的峰值最高也就是在10MB左右徘徊,这对于iPhone6这样的低内存老年机来说,是相当友好的,不会导致低端设备崩溃或者卡顿。boundary
、header
、数据链路等资源的浪费。self.segmentSize = 500 * 1024;
} else if ([Reachability reachableViaWWAN]) {
self.segmentSize = 300 * 1024;
}
WiFi
就固定某个分片大小,如果是流量就固定某个分片大小。然而这种策略并不稳定,因为现在很多手机的网速比WiFi
还快,我们也不能保证WiFi
都是百兆光纤。NSURLSession
的话,是可以保持连接的,省去建立和断开连接的消耗。在iOS
平台上,NSURLSession
支持对一个Host
保持4个连接,所以,如果我们采取并行上传,可以更好的利用当前的网络。NSURLSessionConfiguration
设置的,而且数量最好不要写死。同样的,应该基于当前网络环境,在上传任务开始的时候就计算好最大连接数,并设置给Configuration
。iPhone并行上传:2909 kb/s
uploadTask
,尽管NSURLSession
是可以承受的,也不会造成一个很大的内存峰值。但是我觉得这样并不太好,实际上并不会同时有这么多请求发出。@property (nonatomic, strong) NSMutableArray *successSegments;
/// 待上传队列的数组
@property (nonatomic, strong) NSMutableArray *uploadSegments;
NSURLSession
发起的请求不会超过这个数量。需要注意的是,这个最大任务数是我创建uploadTask
的任务数,并不是最大并发数,最大并发数由NSURLSession
来控制,我不做干预。uploadSegments
中,上传成功后我会从待上传任务数组中取出一条或多条,并保证同时进行的任务始终不超过最大任务数。失败的任务理论上来说也是需要等待上传的,所以我把失败任务也放在uploadSegments
中,插入到队列最下面,这样就保证了待上传任务完成后,继续重试失败任务。successSegments
中,并且始终保持和uploadSegments
没有交集。两个队列中保存的并不是uploadTask
,而是分片的索引,这也就是为什么我给分片命名的时候用索引当做名字的原因。当successSegments
等于分片数量时,就表示所有任务上传完成。文件下载
NSURLSession
是在单独的进程中运行,所以通过此类发起的网络请求,是独立于应用程序运行的,即使App
挂起、kill
也不会停止请求。在下载任务时会比较明显,即便App
被kill
下载任务仍然会继续,并且允许下次启动App
使用这次的下载结果或继续下载。NSURLSession
创建一个downloadTask
,并调用resume
即可开启一个下载任务。NSURLSession *session = [NSURLSession sessionWithConfiguration:config
delegate:self
delegateQueue:[NSOperationQueue mainQueue]];
NSURL *url = [NSURL URLWithString:@"http://vfx.mtime.cn/Video/2017/03/31/mp4/170331093811717750.mp4"];
NSURLRequest *request = [[NSURLRequest alloc] initWithURL:url];
NSURLSessionDownloadTask *downloadTask = [session downloadTaskWithRequest:request];
[downloadTask resume];
suspend
将下载任务挂起,随后调用resume
方法继续下载任务,suspend
和resume
需要是成对的。但是suspend
挂起任务是有超时的,默认为60s,如果超时系统会将TCP
连接断开,我们再调用resume
是失效的。可以通过NSURLSessionConfiguration
的timeoutIntervalForResource
来设置上传和下载的资源耗时。suspend
只针对于下载任务,其他任务挂起后将会重新开始。didFinishDownloadingToURL:
方法是required
,当下载结束后下载文件被写入在Library/Caches
下的一个临时文件,我们需要将此文件移动到自己的目录,临时目录在未来的一个时间会被删掉。- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask
didWriteData:(int64_t)bytesWritten
totalBytesWritten:(int64_t)totalBytesWritten
totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite {
CGFloat progress = (CGFloat)totalBytesWritten / (CGFloat)totalBytesExpectedToWrite;
self.progressView.progress = progress;
}
// 下载完成后回调
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask
didFinishDownloadingToURL:(NSURL *)location {
}
HTTP
协议支持断点续传操作,在开始下载请求时通过请求头设置Range
字段,标示从什么位置开始下载。512kb
的位置开始传输数据,并通过Content-Range
字段告知客户端传输数据的起始位置。downloadTask
任务开始请求后,可以调用cancelByProducingResumeData:
方法可以取消下载,并且可以获得一个resumeData
,resumeData
中存放一些断点下载的信息。可以将resumeData
写到本地,后面通过这个文件可以进行断点续传。NSString *resumePath = [library stringByAppendingPathComponent:[self.downloadURL md5String]];
[self.downloadTask cancelByProducingResumeData:^(NSData * _Nullable resumeData) {
[resumeData writeToFile:resumePath atomically:YES];
}];
downloadTaskWithResumeData:
方法并传入一个resumeData
,可以恢复之前的下载,并重新创建一个downloadTask
任务。NSString *resumePath = [library stringByAppendingPathComponent:[self.downloadURL md5String]];
NSData *resumeData = [[NSData alloc] initWithContentsOfFile:resumePath];
self.downloadTask = [self.session downloadTaskWithResumeData:resumeData];
[self.downloadTask resume];
suspend
和resume
这种方式挂起的任务,downloadTask
是同一个对象,而通过cancel
然后resumeData
恢复的任务,会创建一个新的downloadTask
任务。downloadTaskWithResumeData:
方法恢复下载后,会回调下面的方法。回调参数fileOffset
是上次文件的下载大小,expectedTotalBytes
是预估的文件总大小。didResumeAtOffset:(int64_t)fileOffset
expectedTotalBytes:(int64_t)expectedTotalBytes;
backgroundSessionConfigurationWithIdentifier
方法创建后台上传或后台下载类型的NSURLSessionConfiguration
,并且设置一个唯一标识,需要保证这个标识在不同的session
之间的唯一性。后台任务只支持http
和https
的任务,其他协议的任务并不支持。[NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:[NSOperationQueue mainQueue]];
backgroundSessionConfigurationWithIdentifier
方法创建的NSURLSession
,请求任务将会在系统的单独进程中进行,因此即使App
进程被kill
也不受影响,依然可以继续执行请求任务。如果程序被系统kill调,下次启动并执行didFinishLaunchingWithOptions
可以通过相同的identifier
创建NSURLSession
和NSURLSessionConfiguration
,系统会将新创建的NSURLSession
和单独进程中正在运行的NSURLSession
进行关联。didFinishLaunchingWithOptions
方法时,按照下面方法创建NSURLSession
即可将新创建的Session
和之前的Session
绑定,并自动开始执行之前的下载任务。恢复之前的任务后会继续执行NSURLSession
的代理方法,并执行后面的任务。NSURLSessionConfiguration *config = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:@"identifier"];
[NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:[NSOperationQueue mainQueue]];
return YES;
}
Background Mode
,则不会回调客户端进度。下次进入前台时,会继续回调新的进度。AppDelegate
的回调方法通知应用来刷新UI。由于下载是在一个单独的进程中完成的,即便业务层代码会停止执行,但下载的回调依然会被调用。在回调时,允许用户处理业务逻辑,以及刷新UI。completionHandler
表示刷新结束,所以上层业务要做一些控制逻辑。didFinishDownloadingToURL
的调用时机会比此方法要晚,依然在那个方法里可以判断下载文件。由于项目中可能会存在多个下载任务,所以需要通过identifier
对下载任务进行区分。ViewController *vc = (ViewController *)self.window.rootViewController;
vc.completionHandler = completionHandler;
}
identifier
任务,则创建的session
会将同名的任务都继续执行。NSURLSessionConfiguration
还提供下面的属性,在session
下载任务完成时是否启动App,默认为YES
,如果设置为NO
则后台下载会受到影响。视频地址一般都是从服务端获取的,所以需要先请求接口获取下载地址。这个地址可以是某个接口就已经请求下来的,也可以是某个固定格式拼接的。
现在有很多视频App都是有免流服务的,例如腾讯大王卡、蚂蚁宝卡之类的,免流服务的本质就是对
m3u8
、ts
、mp4
地址重新包一层,请求数据的时候直接请求运营商给的地址,运营商对数据做了一个中转操作。以流视频
m3u8
为例,有了免流地址,先下载m3u8
文件。这个文件一般都是加密的,下载完成后客户端会对m3u8
文件进行decode
,获取到真正的m3u8
文件。m3u8
文件本质上是ts
片段的集合,视频播放播的还是ts
片段。随后对m3u8
文件进行解析,获取到ts
片段地址,并将ts
下载地址转成免流地址后逐个下载,也可以并行下载。m3u8
文件下载后会以固定格式存在文件夹下,文件夹对应被缓存的视频。ts
片命名以数字命名,例如0.ts
,下标从0开始。所有
ts
片段下载完成后,生成本地m3u8
文件。m3u8
文件分为远端和本地两种,远端的就是正常下载的地址,本地m3u8
文件是在播放本地视频的时候传入。格式和普通m3u8
文件差不多,区别在于ts
地址是本地地址,例如下面的地址。
#EXT-X-TARGETDURATION:30
#EXT-X-VERSION:3
#EXTINF:9.28,
0.ts
#EXTINF:33.04,
1.ts
#EXTINF:30.159,
2.ts
#EXTINF:23.841,
3.ts
#EXT-X-ENDLIST
HLS(Http Live Streaming)
是苹果推出的流媒体协议,其中包含两部分,m3u8
文件和ts
文件。使用ts
文件的原因是因为多个ts可以无缝拼接,并且单个ts
可以单独播放。而mp4
由于格式原因,被分割的mp4
文件单独播放会导致画面撕裂或者音频缺失的问题。如果单独下载多个mp4
文件,播放时会导致间断的问题。m3u8
是Unicode
版本的m3u
,是苹果推出的一种视频格式,是一个基于HTTP
的流媒体传输协议。m3u8
协议将一个媒体文件切为多个小文件,并利用HTTP
协议进行数据传输,小文件所在的资源服务器路径存储在.m3u8
文件中。客户端拿到m3u8
文件,即可根据文件中资源文件的路径,分别下载不同的文件。m3u8
文件必须是utf-8
格式编码的,在文件中以#EXT
开头的是标签,并且大小写敏感。以#开头的其他字符串则都会被认为是注释。m3u8
分为点播和直播,点播在第一次请求.m3u8
文件后,将下载下来的ts
片段进行顺序播放即可。直播则需要过一段时间对.m3u8
文件进行一个增量下载,并继续下载后续的ts
文件。m3u8
中有很多标签,下面是项目中用到的一些标签或主要标签。将mp4
或者flv
文件进行切片很简单,直接用ffmpeg
命令切片即可。起始标签,此标签必须在整个文件的开头。
结束标签,此标签必须在整个文件的末尾。
当前文件版本,如果不指定则默认为1
所有
ts
片段最大时长。
当前
ts
片段时长。
#EXT
或#开头的,一般都是ts
片段下载地址。路径可以是绝对路径,也可以是相对路径,我们项目里使用的是绝对路径。但相对路径数据量会相对比较小,只不过看视频的人网速不会太差。segment1.ts
,则表示相对于m3u8
的路径,也就是下面的路径。https://data.vod.itc.cn/segment1.ts
finishTasksAndInvalidate
方法将任务invalidate
。selector:@selector(willTerminateNotification)
name:UIApplicationWillTerminateNotification
object:nil];
- (void)willTerminateNotification {
[self.session getAllTasksWithCompletionHandler:^(NSArray<__kindof NSURLSessionTask *> * _Nonnull tasks) {
if (tasks.count) {
[self.session finishTasksAndInvalidate];
}
}];
}
AFNetworking
,更详细的内容可以参考本期推送的第二条「探秘AFNetworking」一文进行深入的了解。也许你还想看
▼
加入搜狐技术作者天团
千元稿费等你来!
戳这里!☛
您对本文有什么疑问吗?
点我写留言
▼▼▼